Project Name: Transmitter & Receiver: Instant Signal Control
Category: Wireless Communication, IoT, Remote Control
Project Goal: To create a fast, private, direct connection between two devices.
Why You Need This Project: The Simple Scenario
Imagine you have a small apartment, and you want to control your bedroom lamp without buying an expensive smart plug or relying on your home Wi-Fi, which sometimes fails.
- The Problem: Standard Wi-Fi is often slow, requires a router, and uses more battery power.
- The ESP-NOW Solution (Your Project): You can put the Transmitter (with a button) next to your couch in the living room. You put the Receiver (controlling a relay for the lamp) in your bedroom.
- The Benefit: When you press the button, the couch ESP32 instantly and directly “whispers” a message to the bedroom ESP32, telling it to turn the lamp on. It’s fast, private, and works even if your internet goes out!
This project teaches you to build the core wireless “whisper” system, which can be adapted to control anything—not just LEDs and buzzers, but pumps, door locks, or displays!
WHAT WILL YOU LEARN
By building this project, you will master the basics of:
- ESP-NOW: The fast, low-power, direct wireless connection between two ESP32 devices, perfect for remote control.
- Transmitter (TX) vs. Receiver (RX): Setting up one board to send data (the button remote) and the other to listen (the control unit).
- Data Structures (
struct
): Creating a custom “package” to send multiple pieces of information (like “Lamp ON” and “Alarm OFF”) at once. - MAC Addresses: Using the unique hardware address of the Receiver to ensure your command goes to the correct device.
CIRCUIT CONNECTIONS

This map shows you the physical map for building the project. It shows exactly where to plug in every wire for the buttons, LEDs, screen or any components for every exercises so that the software instructions can correctly control all the hardware pieces.
1. Transmitter (Sender) Setup

Component | ESP32 Dev Module | Component function |
LED Control Button (any button) | GPIO4 | Press to tell the Receiver to turn its LED ON. |
Buzzer Control Button (any button) | GPIO5 | Press to tell the Receiver to turn its Buzzer ON. Export to Sheets |
2. Receiver (Listener) Setup

Component | ESP32 Dev Module | Component function |
Controlled LED | GPIO18 | Turns ON when the LED button is pressed on the Transmitter. |
Controlled Buzzer | GPIO19 | Turns ON (makes noise) when the Buzzer button is pressed on the Transmitter. Export to Sheets |
CODE LAB

This section holds the instructions for the ESP32. It’s the “brain” that tells the board exactly what to do, like when to flash a green light, what sound to play, and how to choose a random card. You copy and upload this code so your project can actually work.
Focus on Step-by-Step Flow
- Get the Address: Upload the MAC Address code to your Receiver ESP32 and copy the outputted address from the Serial Monitor.
- Program the Sender: Paste that copied MAC address into the Transmitter ESP32 code and upload the Transmitter sketch.
- Program the Receiver: Finally, upload the dedicated Receiver code to the Receiver ESP32 to complete the setup.
Step 1: Install Library
For this exercise, you don’t need to install any library through the Arduino Library Manager because it already included in the ESP32 board support package:
#include <esp_now.h>
#include <WiFi.h>
These are built-in libraries that come standard when you install the ESP32 board files in the Arduino IDE.
Step 2: Full Code
1. MAC Address Code:
Copy and paste this code then Upload this code in your Receiver ESP32.
#include <WiFi.h>
void setup() {
Serial.begin(115200);
// Set the device to Wi-Fi Station mode
// This is required to initialize the Wi-Fi stack and get the MAC address
WiFi.mode(WIFI_STA);
// Get and print the MAC address
String macAddress = WiFi.macAddress();
Serial.println("");
Serial.println("---------------------------------");
Serial.println("ESP32 MAC Address Finder");
Serial.println("---------------------------------");
Serial.print("MAC Address (colon format): ");
Serial.println(macAddress);
// Print the MAC address in the format needed for the ESP-NOW code
Serial.print("MAC Address (ESP-NOW Code Format): {0x");
for (int i = 0; i < macAddress.length(); i++) {
// Only print the hex characters, ignore the colons
if (macAddress.charAt(i) != ':') {
Serial.print(macAddress.charAt(i));
}
// Add the 0x and comma separators for the code format
if (i % 3 == 1 && i < macAddress.length() - 1) {
Serial.print(", 0x");
}
}
Serial.println("};");
Serial.println("---------------------------------");
}
void loop() {
// The MAC address is only retrieved in setup(), so the loop can be empty.
delay(5000);
}
After upload, you need to copy the MAC Address in the Serial Monitor. Copy the ESP-NOW format. Example Output:
MAC Address (colon format): 30:AE:A4:CC:DD:EE
MAC Address (ESP-NOW Code Format): {0x30, 0xAE, 0xA4, 0xCC, 0xDD, 0xEE};
You will paste the MAC Address in the Transmitter(Sender) ESP32 code.
Why do you need to get the MAC Address first?
In the simplest way, we need the MAC Address because it acts as the unique street address of the Receiver ESP32.
Here is why:
- Direct Communication: When you use ESP-NOW, your Transmitter (the remote) isn’t talking to a router; it’s talking directly to the Receiver (the control unit).
- Required Destination: To send a message directly, the Transmitter needs to know exactly who it’s sending the message to.
- The MAC is the Name Tag: Every Wi-Fi device (including your ESP32) has a unique, permanent MAC Address assigned at the factory. It’s like the serial number or the permanent address of that specific device.
Think of it this way:
You want to send a secret letter (your button command) to your friend’s house (the Receiver ESP32).
- If you just yell the letter into the air, no one knows who it’s for.
- The MAC Address is like writing the exact street address on the envelope.
By putting the Receiver’s MAC Address into the Transmitter’s code, you are telling the Transmitter: “Send this wireless command to the device at this specific address, and nowhere else.” This makes the communication fast and private!
2. ESP32 Transmitter (Sender) Code:
This code reads the button states and sends the entire data structure every time a button’s state changes.
Copy and paste this code then Upload this code in your Transmitter (Sender) ESP32.
Before you upload the code, make sure to change this in the code. Change with the MAC Address of the receiver ESP32 you get just now.
After you changed, then you can upload the code.

#include <esp_now.h>
#include <WiFi.h>
// 1. **REPLACE THIS with your Receiver's MAC Address**
uint8_t receiverAddress[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}; // Your MAC here!
// GPIO Pin definitions
const int LED_BUTTON_PIN = 4;
const int BUZZER_BUTTON_PIN = 5;
// Data structure (MUST be the same on TX and RX)
typedef struct struct_message {
bool ledState;
bool buzzerState;
} struct_message;
struct_message myData;
struct_message prevData = {false, false}; // To track previous state for change detection
// Callback function executed when data is sent
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
Serial.print("\r\nLast Packet Send Status:\t");
Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail");
}
void setup() {
Serial.begin(115200);
// Set up input pins
pinMode(LED_BUTTON_PIN, INPUT);
pinMode(BUZZER_BUTTON_PIN, INPUT);
// Set device as a Wi-Fi Station
WiFi.mode(WIFI_STA);
// Initialize ESP-NOW
if (esp_now_init() != ESP_OK) {
Serial.println("Error initializing ESP-NOW");
return;
}
// Register the send callback function
esp_now_register_send_cb(OnDataSent);
// Register peer (the receiver)
esp_now_peer_info_t peerInfo;
memcpy(peerInfo.peer_addr, receiverAddress, 6);
peerInfo.channel = 0;
peerInfo.encrypt = false;
// Add peer
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
Serial.println("Failed to add peer");
return;
}
}
void loop() {
// Read current button states
bool currentLedState = digitalRead(LED_BUTTON_PIN);
bool currentBuzzerState = digitalRead(BUZZER_BUTTON_PIN);
// Check if a state has changed
if (currentLedState != prevData.ledState || currentBuzzerState != prevData.buzzerState) {
// Update the data structure
myData.ledState = currentLedState;
myData.buzzerState = currentBuzzerState;
// Send message via ESP-NOW
esp_err_t result = esp_now_send(receiverAddress, (uint8_t *)&myData, sizeof(myData));
if (result == ESP_OK) {
Serial.println("Sent with success");
} else {
Serial.println("Error sending the data");
}
// Update the previous state
prevData.ledState = currentLedState;
prevData.buzzerState = currentBuzzerState;
}
delay(50); // Small delay to debounce and avoid excessive sending
}
3. ESP32 Receiver(Listener) Code:
This code listens for incoming ESP-NOW messages and updates the LED and Buzzer based on the received data.
#include <esp_now.h>
#include <WiFi.h>
// GPIO Pin definitions
const int LED_PIN = 18;
const int BUZZER_PIN = 19;
// Data structure (MUST be the same on TX and RX)
typedef struct struct_message {
bool ledState;
bool buzzerState;
} struct_message;
struct_message incomingData;
// Callback function executed when data is received
void OnDataRecv(const uint8_t *mac, const uint8_t *incomingDataPtr, int len) {
memcpy(&incomingData, incomingDataPtr, sizeof(incomingData));
Serial.print("Bytes received: ");
Serial.println(len);
// Control the LED and Buzzer based on the received data
digitalWrite(LED_PIN, incomingData.ledState);
digitalWrite(BUZZER_PIN, incomingData.buzzerState);
Serial.print("LED State: ");
Serial.println(incomingData.ledState ? "ON" : "OFF");
Serial.print("Buzzer State: ");
Serial.println(incomingData.buzzerState ? "ON" : "OFF");
}
void setup() {
Serial.begin(115200);
// Set up output pins
pinMode(LED_PIN, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
// Ensure outputs are off initially
digitalWrite(LED_PIN, LOW);
digitalWrite(BUZZER_PIN, LOW);
// Set device as a Wi-Fi Station
WiFi.mode(WIFI_STA);
// Initialize ESP-NOW
if (esp_now_init() != ESP_OK) {
Serial.println("Error initializing ESP-NOW");
return;
}
// Register the receive callback function
esp_now_register_recv_cb(OnDataRecv);
}
void loop() {
// Everything is handled in the OnDataRecv callback function,
// so the loop can be empty or used for other tasks.
delay(10);
}
Step 3: Upload and Test
- Upload Receiver Code: Upload the code to your Receiver board and keep it powered on.
- Upload Transmitter Code: Upload the code to your Transmitter board (with the correct MAC).
- Test: Press the buttons on the Transmitter. The LED and Buzzer on the Receiver should respond instantly!
Troubleshooting Guide
This guide is your fast-fix manual for when errors occur. It helps you quickly diagnose why something isn’t working, like a dark screen or a silent buzzer—and provides simple steps to correct the problem.
Problem | What is Happening? | Simple Solution to Try |
TX shows “Failed to add peer” | The MAC address in the TX code is wrong. | 1. MAC Address: Double-check the 6-byte address for typos. 2. Format: Ensure it is in the 0xXX, 0xXX, ... format. |
TX shows “Delivery Success,” but the RX doesn’t activate outputs. | The data is sent successfully, but the RX wiring is wrong. | 1. RX Wiring: Check the LED/Buzzer wiring on GPIO18 and GPIO19. 2. Output States: Ensure your outputs are wired correctly (LED with resistor, buzzer). |
The output is always ON, even when the button is released. | The button wiring is backward. | TX Button Wiring: The button must connect the pin (4 or 5) |
RX shows data received, but the pin states don’t change. | The received data isn’t being applied to the pins. | Struct Match: Ensure the struct_message is IDENTICAL in both TX and RX files (variable names and types must match exactly). |
Buy from:
Myduino AIoT Education Kit (click here) from myduino.com